/**
 * Fraction
 *
 * @author Chris Pratt
 *
 * 2/14/2005
 */
package com.anodyzed.onyx.util;

import java.text.ParseException;

@SuppressWarnings("serial")
public class Fraction extends Number {
  private double dec;
  private long w;
  private long n;
  private long d;

  /**
   * No-arg Bean Constructor
   */
  public Fraction () {
    w = n = d = 0;
    dec = 0.0;
  } //Fraction

  /**
   * Constructor - Algorithm adapted from
   *  http://www.ozgrid.com/forum/archive/index.php/t-22530.html
   *
   * @param dec The Double to represent as a Fraction
   */
  public Fraction (double dec) {
    setDecimal(dec);
  } //Fraction

  /**
   * Constructor
   *
   * @param n The Numerator of the Fraction
   * @param d The Denominator of the Fraction
   */
  public Fraction (long n,long d) {
    this(0,n,d);
  } //Fraction

  /**
   * Constructor
   * 
   * @param w The Whole Portion of the Fraction
   * @param n The Numerator of the Fraction
   * @param d The Denominator of the Fraction
   */
  public Fraction (long w,long n,long d) {
    this.w = w;
    this.n = n;
    this.d = d;
    this.dec = w + ((double)n / (double)d);
  } //Fraction

  /**
   * Constructor
   *
   * @param frac The Fraction String
   * @throws ParseException If the fraction can't be parsed
   */
  public Fraction (String frac) throws ParseException {
    if(frac.indexOf('/') == -1) {
      setDecimal(Double.parseDouble(frac));
    } else {
      long x = 0;
      w = 0;
      n = -1;
      d = -1;
      char[] ch = frac.toCharArray();
      boolean neg = (ch[0] == '-');
      int i = (neg | (ch[0] == '+')) ? 1 : 0;
      for(;i < ch.length;i++) {
        switch(ch[i]) {
          case ' ':
            w = x;
            x = 0;
            break;
          case '/':
            n = x;
            x = 0;
            break;
          case '0':
          case '1':
          case '2':
          case '3':
          case '4':
          case '5':
          case '6':
          case '7':
          case '8':
          case '9':
            x = (x * 10) + Character.digit(ch[i],10);
            break;
          default:
            throw new ParseException("\'" + ch[i] + "\' is not a valid character in a Fraction - " + frac,i);
        }
      }
      d = x;
      if((n != -1) && (d != -1) && (d != 0)) {
        dec = w + ((double)n / (double)d);
        if(neg) {
          dec = -dec;
          if(w != 0) {
            w = -w;
          } else {
            n = -n;
          }
        }
      } else {
        throw new ParseException("Malformed Fraction - " + frac,i);
      }
      reduce();
    }
  } //Fraction

  /**
   * Return the Whole Number portion of the Fraction
   *
   * @return The Non-fractional part
   */
  public long getWhole () {
    return w;
  } //getWhole

  /**
   * Set the Whole part of the Fraction
   *
   * @param w The Whole Number
   */
  public void setWhole (long w) {
    this.w = w;
  } //setWhole

  /**
   * Get the Numerator of the Reduced Fraction
   *
   * @return The Fraction's Numerator
   */
  public long getNumerator () {
    return n;
  } //getNumerator

  /**
   * Set the Numerator of the Fraction
   *
   * @param n The fraction numerator
   */
  public void setNumerator (long n) {
    this.n = n;
    if(d != 0) {
      dec = w + ((double)n / (double)d);
    } else {
      dec = Double.NaN;
    }
  } //setNumerator

  /**
   * Get the Denominator of the Reduced Fraction
   *
   * @return The Fraction's Denominator
   */
  public long getDenominator () {
    return d;
  } //getDenominator

  /**
   * Set the Denominator of the Fraction
   *
   * @param d The fraction denominator
   */
  public void setDenominator (long d) {
    this.d = d;
    if(d != 0) {
      dec = w + ((double)n / (double)d);
    } else {
      dec = Double.NaN;
    }
  } //setDenominator

  /**
   * Number: Returns the value of the specified number as a <code>double</code>.
   * This may involve rounding. 
   *
   * @return the numeric value represented by this Object after conversion to 
   *         type <code>double</code>
   */
  @Override
  public double doubleValue () {
    return dec;
  } //doubleValue

  /**
   * Number: Returns the value of the specified number as a <code>float</code>. 
   * This may involve rounding. 
   *
   * @return the numeric value represented by this Object after conversion to 
   *         type <code>float</code>
   */
  @Override
  public float floatValue () {
    return (float)dec;
  } //floatValue

  /**
   * Number: Returns the value of the specified number as a <code>int</code>. 
   * This may involve rounding. 
   *
   * @return the numeric value represented by this Object after conversion to 
   *         type <code>int</code>
   */
  @Override
  public int intValue () {
    return (int)dec;
  } //intValue

  /**
   * Number: Returns the value of the specified number as a <code>long</code>. 
   * This may involve rounding. 
   *
   * @return the numeric value represented by this Object after conversion to 
   *         type <code>long</code>
   */
  @Override
  public long longValue () {
    return (long)dec;
  } //longValue

  /**
   * Set the Fraction amount using a Double Precision Floating Point value
   *
   * Algorithm adapted from
   *   http://www.ozgrid.com/forum/archive/index.php/t-22530.html
   *
   * @param dec The new value to represent as a Fraction
   */
  public void setDecimal (double dec) {
     this.dec = dec;
     if(dec >= 1.0) {
       w = (long)Math.floor(dec);
       dec -= w;
     } else {
       w = 0;
     }
     if(dec > 0.0) {
       double z = dec;
       double fd = 1.0;
       double fn;
       double pd = 0.0;
       double temp;
       do {
         z = (1.0 / (z - Math.floor(z)));
         temp = fd;
         fd = Math.floor(z) * fd + pd;
         pd = temp;
         fn = Math.floor(dec * fd + 0.5);
       } while((Math.abs(dec - (fn / fd)) >= 1.0e-14) && (z != Math.floor(z)));
       n = (long)Math.floor(fn);
       d = (long)Math.floor(fd);
       if(n > d) {
         w += n / d;
         n = n % d;
       }
       reduce();
     } else {
       n = d = 0;
     }
  } //setDecimal

  /**
   * Reduce the Fraction
   */
  private void reduce () {
    if(n == 0) {
      d = 1;
    } else {
      long gcd = gcd(n,d);
      n /= gcd;
      d /= gcd;
      if(d < 0) {
        n = -n;
        d = -d;
      }
    }
  } //reduce

  /**
   * Compute the Greatest Common Divisor between two numbers
   *
   * @param left The First Parameter
   * @param rite The Second Parameter
   * @return The Greatest Common Divisor of the given parameters
   */
  public static long gcd (long left,long rite) {
    left = Math.abs(left);
    rite = Math.abs(rite);
    if((left > 2) && (rite > 2)) {
      long temp;
      do {
        if(left < rite) {
          temp = left;
          left = rite;
          rite = temp;
        }
        left %= rite;
      } while(left != 0);
      return rite;
    } else {
        // If either argument is 0 or 1, the GCD is 1
      return 1;
    }
  } //gcd

  /**
   * Return the String Representation of the Fraction
   *
   * @return The String Representation of the Fraction
   */
  @Override
  public String toString () {
    StringBuilder buf = new StringBuilder();
    if(w != 0) {
      buf.append(w);
    }
    if((n != d)) {
      if(buf.length() > 0) {
        buf.append(' ');
      }
      buf.append(n).append('/').append(d);
    }
    return buf.toString();
  } //toString

  /**
   * Convenience Method to Format a double precision real value to a
   *  Fractional String
   *
   * @param dec The double to Convert
   * @return The Fractional String representing the double
   */
  public static String toString (double dec) {
    return new Fraction(dec).toString();
  } //toString

  /**
   * Convenience Method to Normalize a numeric string as a Fractional String
   *
   * @param frac The String to Convert
   * @return The Fractional String
   */
  public static String toString (String frac) {
    try {
      return new Fraction(frac).toString();
    } catch(ParseException x) {
      return null;
    }
  } //toString

  /**
   * Convenience Method to convert a Fraction String to a double-precision 
   * floating-point value. 
   *
   * @param frac The String to Convert
   * @return double value
   */
  public static double toDouble (String frac) {
    try {
      return new Fraction(frac).doubleValue();
    } catch(ParseException x) {
      return Double.NaN;
    }
  } //toDouble

  /**
   * Determine if the supplied character could be part of a number.  Supports 
   * Whole, Decimal & Fractional notation (Currently does not support Scientific
   * or Hexidecimal) 
   *  
   * Supports: 
   * <ul> 
   *   <li>12</li>
   *   <li>13.57</li>
   *   <li>.5</li>
   *   <li>1/2</li>
   *   <li>1 7/8</li>
   * </ul>
   *
   * @param ch The character in question
   * @return true if a digit, '.', '/' or ' '
   */
  public static boolean isNumeric (char ch) {
    return Character.isDigit(ch) || (ch == '.') || (ch == '/') || (ch == ' ');
  } //isNumeric

  /**
   * Program Entry-point
   *
   * @param args Command Line Arguments
   * @throws ParseException if the String can't be parsed
   */
  public static void main (String[] args) throws ParseException {
    StringBuilder buf = new StringBuilder();
    for(int i = 0;i < args.length;i++) {
      if(i > 0) {
        buf.append(' ');
      }
      buf.append(args[i]);
    }
    Fraction f = new Fraction(buf.toString());
    System.out.println(f.toString());
    System.out.println(f.doubleValue());
//    System.out.println(new Fraction(Misc.getIntArg("-w",args,0),Misc.getIntArg("-n",args,0),Misc.getIntArg("-d",args,1)).toString());
  } //main

} //*Fraction
